agora inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH] ERROR out early on attempt to touch user indexes while they are being (re)built.
1105+ messages / 3 participants
[nested] [flat]

* [PATCH] ERROR out early on attempt to touch user indexes while they are being (re)built.
@ 2019-09-12 14:35  Arseny Sher <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Arseny Sher @ 2019-09-12 14:35 UTC (permalink / raw)

Existing ReindexIsProcessingIndex check is consulted only in genam.c and thus
enforced only for system catalogs. Check it also in the planner, so that indexes
which are currently being rebuilt are never tried. Also cock SetReindexProcessing
in index_create to defend from index self usage during its creation.

Without this, VACUUM FULL or just CREATE INDEX might fail with something like

ERROR:  could not read block 3534 in file "base/41366676/56697497": read only 0 of 8192 bytes

if there are indexes which usage can be considered during these very
indexes (re)building, i.e. index expression scans indexed table.

Such error might seem scary, so catch this earlier.
---
 src/backend/catalog/index.c                | 22 ++++++++++++++++++++--
 src/backend/optimizer/util/plancat.c       |  9 +++++++++
 src/test/regress/expected/create_index.out | 15 +++++++++++++++
 src/test/regress/sql/create_index.sql      | 13 +++++++++++++
 4 files changed, 57 insertions(+), 2 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 3e1d40662d..5bc764ce46 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1174,7 +1174,22 @@ index_create(Relation heapRelation,
 	}
 	else
 	{
-		index_build(heapRelation, indexRelation, indexInfo, false, true);
+		/* ensure SetReindexProcessing state isn't leaked */
+		PG_TRY();
+		{
+			/* Suppress use of the target index while building it */
+			SetReindexProcessing(heapRelationId, indexRelationId);
+
+			index_build(heapRelation, indexRelation, indexInfo, false, true);
+		}
+		PG_CATCH();
+		{
+			/* Make sure flag gets cleared on error exit */
+			ResetReindexProcessing();
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+		ResetReindexProcessing();
 	}
 
 	/*
@@ -1379,7 +1394,10 @@ index_concurrently_build(Oid heapRelationId,
 	indexInfo->ii_Concurrent = true;
 	indexInfo->ii_BrokenHotChain = false;
 
-	/* Now build the index */
+	/*
+	 * Now build the index
+	 * SetReindexProcessing is not required since indisvalid is false anyway
+	 */
 	index_build(heapRel, indexRelation, indexInfo, false, true);
 
 	/* Close both the relations, but keep the locks */
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index cf1761401d..e42471cac8 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -27,6 +27,7 @@
 #include "access/xlog.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
+#include "catalog/index.h"
 #include "catalog/heap.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_proc.h"
@@ -199,6 +200,14 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 			indexRelation = index_open(indexoid, lmode);
 			index = indexRelation->rd_index;
 
+			if (ReindexIsProcessingIndex(indexoid))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("cannot use index %s which is currently being reindexed",
+								RelationGetRelationName(indexRelation)),
+						 errhint("Probably index expressions include functions "
+								 "accessing indexed table itself, but they must be immutable.")));
+
 			/*
 			 * Ignore invalid indexes, since they can't safely be used for
 			 * queries.  Note that this is OK because the data structure we
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 324db1b6ae..f9aeb97ed8 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1323,6 +1323,21 @@ create unique index hash_f8_index_1 on hash_f8_heap(abs(random));
 create unique index hash_f8_index_2 on hash_f8_heap((seqno + 1), random);
 create unique index hash_f8_index_3 on hash_f8_heap(random) where seqno > 1000;
 --
+-- Create an index which might consider using this very index during the build.
+--
+-- primary key ensures relhasindex is set
+CREATE TABLE pears(f1 int primary key, f2 int);
+INSERT INTO pears SELECT i, i+1 FROM generate_series(1, 100) i;
+CREATE FUNCTION pears_f(i int) RETURNS int LANGUAGE SQL IMMUTABLE AS $$
+  SELECT f1 FROM pears WHERE pears.f2 = 42
+$$;
+CREATE index ON pears ((pears_f(f1)));
+ERROR:  cannot use index pears_pears_f_idx which is currently being reindexed
+HINT:  Probably index expressions include functions accessing indexed table itself, but they must be immutable.
+CONTEXT:  SQL function "pears_f" during startup
+DROP TABLE pears;
+DROP FUNCTION pears_f;
+--
 -- Try some concurrent index builds
 --
 -- Unfortunately this only tests about half the code paths because there are
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index f96bebf410..76a781f6b0 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -446,6 +446,19 @@ create unique index hash_f8_index_2 on hash_f8_heap((seqno + 1), random);
 create unique index hash_f8_index_3 on hash_f8_heap(random) where seqno > 1000;
 
 --
+-- Create an index which might consider using this very index during the build.
+--
+-- primary key ensures relhasindex is set
+CREATE TABLE pears(f1 int primary key, f2 int);
+INSERT INTO pears SELECT i, i+1 FROM generate_series(1, 100) i;
+CREATE FUNCTION pears_f(i int) RETURNS int LANGUAGE SQL IMMUTABLE AS $$
+  SELECT f1 FROM pears WHERE pears.f2 = 42
+$$;
+CREATE index ON pears ((pears_f(f1)));
+DROP TABLE pears;
+DROP FUNCTION pears_f;
+
+--
 -- Try some concurrent index builds
 --
 -- Unfortunately this only tests about half the code paths because there are
-- 
2.11.0


--=-=-=--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2019-11-08 13:12  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size.

In psql, include a new "Free" column in \db+ output.
---
 doc/src/sgml/func.sgml          | 21 ++++++++
 doc/src/sgml/ref/psql-ref.sgml  |  2 +-
 src/backend/utils/adt/dbsize.c  | 94 +++++++++++++++++++++++++++++++++
 src/bin/psql/describe.c         | 11 ++--
 src/include/catalog/pg_proc.dat |  8 +++
 5 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..3a2f47c50ec 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
-- 
2.47.2


--vqKhBUYnSUFh3RIa--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v4] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  11 ++-
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 171 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..9bd8667c2d9 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--mxigUiQ2jY+Id+Rf--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v5] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 102 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 189 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 72038fc835f..959b0b673ab 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7c05afd4719..1ea67b3659f 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..b395824ca3f 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+		elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
+	}
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e1449654f96..36486417a48 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 190000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..78c03ea6412 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7866,6 +7866,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--sW4eVZZF7egqrnKF--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Add pg_tablespace_avail() functions
@ 2025-03-14 15:29  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func.sgml                   | 21 ++++++
 doc/src/sgml/ref/psql-ref.sgml           |  2 +-
 src/backend/utils/adt/dbsize.c           | 94 ++++++++++++++++++++++++
 src/bin/psql/describe.c                  | 11 ++-
 src/include/catalog/pg_proc.dat          |  8 ++
 src/test/regress/expected/tablespace.out | 21 ++++++
 src/test/regress/sql/tablespace.sql      | 10 +++
 7 files changed, 163 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..0b4456ad958 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..30e0cb8d111 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+		return -1;
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+		return -1;
+
+	return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 180000)
+			appendPQExpBuffer(&buf,
+							  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 42e427f8fe8..9d64da6bfb8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7680,6 +7680,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'disk stats for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6709ed794df 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..3fcd4bb00ff 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.47.2


--65NGEM4fhwMw73YU--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH] Do not push the active snapshot for copy_table_data before it's needed.
@ 2026-04-10 10:17  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Antonin Houska @ 2026-04-10 10:17 UTC (permalink / raw)

make_new_heap() can launch parallel workers when building index on TOAST
relation. If the snapshot for copy_table_data() is already the active
snapshot, the workers can incorrectly consider it their transaction snapshot.
---
 src/backend/commands/repack.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 58e3867246f..08fd132a4e3 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -1013,8 +1013,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Wait until the worker has the initial snapshot and retrieve it.
 		 */
 		snapshot = get_initial_snapshot(decoding_worker);
-
-		PushActiveSnapshot(snapshot);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1038,6 +1036,8 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	NewHeap = table_open(OIDNewHeap, NoLock);
 
 	/* Copy the heap data into the new table in the desired order */
+	if (snapshot)
+		PushActiveSnapshot(snapshot);
 	copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
 					&swap_toast_by_content, &frozenXid, &cutoffMulti);
 
-- 
2.47.3


--=-=-=--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--





^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v7] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  29 ++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ad9c8affb4f..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--Vrlrh/SZa9M0BUO9--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread

* [PATCH v6] Add pg_tablespace_avail() functions
@ 2026-07-21 13:08  Christoph Berg <[email protected]>
  0 siblings, 0 replies; 1105+ messages in thread

From: Christoph Berg @ 2026-07-21 13:08 UTC (permalink / raw)

This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.

Permissions required match those from pg_tablespace_size().

In psql, include a new "Free" column in \db+ output.

Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_size() function.
---
 doc/src/sgml/func/func-admin.sgml        |  21 +++++
 doc/src/sgml/ref/psql-ref.sgml           |   2 +-
 src/backend/utils/adt/dbsize.c           | 110 +++++++++++++++++++++++
 src/bin/psql/describe.c                  |  32 +++++--
 src/include/catalog/pg_proc.dat          |   8 ++
 src/test/regress/expected/tablespace.out |  21 +++++
 src/test/regress/sql/tablespace.sql      |  10 +++
 7 files changed, 197 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..de29fb2b4fc 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_tablespace_avail</primary>
+        </indexterm>
+        <function>pg_tablespace_avail</function> ( <type>name</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para role="func_signature">
+        <function>pg_tablespace_avail</function> ( <type>oid</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the available disk space in the file system hosting the tablespace with the
+        specified name or OID. To use this function, you must
+        have <literal>CREATE</literal> privilege on the specified tablespace
+        or have privileges of the <literal>pg_read_all_stats</literal> role,
+        unless it is the default tablespace for the current database.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 56c2692e618..c7efa652052 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
         If <literal>x</literal> is appended to the command name, the results
         are displayed in expanded mode.
         If <literal>+</literal> is appended to the command name, each tablespace
-        is listed with its associated options, on-disk size, permissions and
+        is listed with its associated options, on-disk size and free disk space, permissions and
         description.
         </para>
         </listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..ae061fbf56b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,12 @@
 #include "postgres.h"
 
 #include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#include <errhandlingapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
 
 #include "access/htup_details.h"
 #include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * Return available disk space for tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+	char		tblspcPath[MAXPGPATH];
+	AclResult	aclresult;
+#ifdef WIN32
+	ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+	struct statvfs fst;
+#endif
+
+	/*
+	 * User must have privileges of pg_read_all_stats or have CREATE privilege
+	 * for target tablespace, either explicitly granted or implicitly because
+	 * it is default for current database.
+	 */
+	if (tblspcOid != MyDatabaseTableSpace &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_TABLESPACE,
+						   get_tablespace_name(tblspcOid));
+	}
+
+	if (tblspcOid == DEFAULTTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "base");
+	else if (tblspcOid == GLOBALTABLESPACE_OID)
+		snprintf(tblspcPath, MAXPGPATH, "global");
+	else
+		snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+				 TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+	if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+	{
+		_dosmaperr(GetLastError());
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+	if (statvfs(tblspcPath, &fst) < 0)
+	{
+		if (errno == ENOENT)
+			return -1;
+		else
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
+	}
+
+	return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+	Oid			tblspcOid = PG_GETARG_OID(0);
+	int64		avail;
+
+	/*
+	 * Not needed for correctness, but avoid non-user-facing error message
+	 * later if the tablespace doesn't exist.
+	 */
+	if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+	Name		tblspcName = PG_GETARG_NAME(0);
+	Oid			tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+	int64		avail;
+
+	avail = calculate_tablespace_avail(tblspcOid);
+
+	if (avail < 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT64(avail);
+}
+
+
 /*
  * calculate size of (one fork of) a relation
  *
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a2f09c26369..06eda474100 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
 	appendPQExpBuffer(&buf,
 					  "SELECT spcname AS \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
-					  "  pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
+					  "  pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
 					  gettext_noop("Name"),
 					  gettext_noop("Owner"),
 					  gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
 		printACLColumn(&buf, "spcacl");
 		appendPQExpBuffer(&buf,
 						  ",\n  spcoptions AS \"%s\""
-						  ",\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
-						  ",\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+						  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+						  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
+						  "       ELSE 'No Access'"
+						  "  END as \"%s\"",
 						  gettext_noop("Options"),
-						  gettext_noop("Size"),
+						  gettext_noop("Size"));
+		if (pset.sversion >= 200000)
+			appendPQExpBuffer(&buf,
+							  ",\n  CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
+							  "               pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
+							  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
+							  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
+							  "       ELSE 'No Access'"
+							  "  END as \"%s\"",
+							  gettext_noop("Free"));
+		appendPQExpBuffer(&buf,
+						  ",\n  pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
 						  gettext_noop("Description"));
 	}
 
 	appendPQExpBufferStr(&buf,
-						 "\nFROM pg_catalog.pg_tablespace\n");
+						 "\nFROM pg_catalog.pg_tablespace tblspc\n");
+	if (verbose)
+		appendPQExpBufferStr(&buf,
+							 "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
+							 "  WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
 
 	if (!validateSQLNamePattern(&buf, pattern, false, false,
 								NULL, "spcname", NULL,
@@ -986,7 +1005,8 @@ listAllDbs(const char *pattern, bool verbose)
 	printACLColumn(&buf, "d.datacl");
 	if (verbose)
 		appendPQExpBuffer(&buf,
-						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n"
+						  ",\n  CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n"
+						  "               pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
 						  "       THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n"
 						  "       ELSE 'No Access'\n"
 						  "  END as \"%s\""
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..9c04c88225f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7859,6 +7859,14 @@
   descr => 'total disk space usage for the specified tablespace',
   proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+  descr => 'free disk space for the specified tablespace',
+  proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+  proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
 { oid => '2324', descr => 'total disk space usage for the specified database',
   proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
   proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index f0dd25cdf0c..12a78c77e05 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
  {random_page_cost=3.0}
 (1 row)
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+ ?column? | ?column? | pg_tablespace_size 
+----------+----------+--------------------
+ t        | t        |                  0
+(1 row)
+
+SELECT pg_tablespace_size('missing');
+ERROR:  tablespace "missing" does not exist
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT pg_tablespace_avail('missing');
+ERROR:  tablespace "missing" does not exist
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 -- This returns a relative path as of an effect of allow_in_place_tablespaces,
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index c43a59e5957..91152335459 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
 -- check to see the parameter was used
 SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
 
+-- check size functions
+SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
+       pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
+       pg_tablespace_size('regress_tblspacewith'); -- empty
+SELECT pg_tablespace_size('missing');
+SELECT pg_tablespace_avail('pg_default') > 1_000_000,
+       pg_tablespace_avail('pg_global') > 1_000_000,
+       pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
+SELECT pg_tablespace_avail('missing');
+
 -- drop the tablespace so we can re-use the location
 DROP TABLESPACE regress_tblspacewith;
 
-- 
2.53.0


--mAXE2ujK8RZQKpWZ--






^ permalink  raw  reply  [nested|flat] 1105+ messages in thread


end of thread, other threads:[~2026-07-21 13:08 UTC | newest]

Thread overview: 1105+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2019-09-12 14:35 [PATCH] ERROR out early on attempt to touch user indexes while they are being (re)built. Arseny Sher <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-04-10 10:17 [PATCH] Do not push the active snapshot for copy_table_data before it's needed. Antonin Houska <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v7] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
2026-07-21 13:08 [PATCH v6] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox